/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * inetconv - convert inetd.conf entries into smf(7) service manifests,
 *            import them into smf(7) repository
 */

#include <sys/types.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <pwd.h>
#include <grp.h>
#include <errno.h>
#include <limits.h>
#include <locale.h>
#include <libintl.h>
#include <libscf.h>
#include <inetsvc.h>
#include <rpc/nettype.h>

/* exit codes */
#define	EXIT_SUCCESS	0	/* succeeded */
#define	EXIT_USAGE	1	/* bad options */
#define	EXIT_ERROR_CONV 2	/* error(s) coverting inetd.conf entries */
#define	EXIT_ERROR_IMP	3	/* error(s) importing manifests */
#define	EXIT_ERROR_SYS	4	/* system error */
#define	EXIT_ERROR_ENBL 5	/* error(s) enabling services */

#ifndef TEXT_DOMAIN
#define	TEXT_DOMAIN		"SUNW_OST_OSCMD"
#endif

#define	MAIN_CONFIG		"/etc/inet/inetd.conf"
#define	ALT_CONFIG		"/etc/inetd.conf"

#define	MANIFEST_DIR		"/lib/svc/manifest/network"
#define	MANIFEST_RPC_DIR	MANIFEST_DIR  "/rpc"
#define	SVCCFG_PATH		"/usr/sbin/svccfg"

#define	RPCBIND_FMRI		"svc:/network/rpc/bind"

/* maximum allowed length of an inetd.conf format line */
#define	MAX_SRC_LINELEN		32768

/* Version of inetconv, used as a marker in services we generate */
#define	INETCONV_VERSION	1

struct inetconfent {
	/* fields as read from inetd.conf format line */
	char *service;
	char *endpoint;
	char *protocol;
	char *wait_status;
	char *username;
	char *server_program;
	char *server_args;
	/* information derived from above fields */
	boolean_t wait;
	boolean_t isrpc;
	int rpc_low_version;
	int rpc_high_version;
	char *rpc_prog;
	char *groupname;
	char *exec;
	char *arg0;
};

struct fileinfo {
	FILE *fp;
	char *filename;
	int lineno;
	int failcnt;
};

static char *progname;

static boolean_t import = B_TRUE;

/* start of manifest XML template strings */
static const char xml_header[] =
"<?xml version='1.0'?>\n"
"<!DOCTYPE service_bundle SYSTEM "
"'/usr/share/lib/xml/dtd/service_bundle.dtd.1'>\n";

static const char xml_comment[] =
"<!--\n"
"    Service manifest for the %s service.\n"
"\n"
"    Generated by inetconv(8) from inetd.conf(5).\n"
"-->\n\n";

static const char xml_service_bundle[] =
"<service_bundle type='manifest' name='inetconv:%s'>\n\n";

static const char xml_service_name[] =
"<service\n"
"	name='network/%s'\n"
"	type='service'\n"
"	version='1'>\n\n";

static const char xml_dependency[] =
"	<dependency\n"
"		name='%s'\n"
"		grouping='require_all'\n"
"		restart_on='restart'\n"
"		type='service'>\n"
"		<service_fmri value='%s' />\n"
"	</dependency>\n\n";

static const char xml_instance[] =
"	<create_default_instance enabled='true'/>\n\n";

static const char xml_restarter[] =
"	<restarter>\n"
"		<service_fmri value='%s' />\n"
"	</restarter>\n\n";

static const char xml_exec_method_start[] =
"	<!--\n"
"	    Set a timeout of 0 to signify to inetd that we don't want to\n"
"	    timeout this service, since the forked process is the one that\n"
"	    does the service's work. This is the case for most/all legacy\n"
"	    inetd services; for services written to take advantage of SMF\n"
"	    capabilities, the start method should fork off a process to\n"
"	    handle the request and return a success code.\n"
"	-->\n"
"	<exec_method\n"
"		type='method'\n"
"		name='%s'\n"
"		%s='%s'\n"
"		timeout_seconds='0'>\n"
"		<method_context>\n"
"			<method_credential %s='%s' group='%s' />\n"
"		</method_context>\n";

static const char xml_arg0[] =
"		<propval name='%s' type='astring'\n"
"		    value='%s' />\n";

static const char xml_exec_method_end[] =
"	</exec_method>\n\n";

static const char xml_exec_method_disable[] =
"	<!--\n"
"	    Use inetd's built-in kill support to disable services.\n"
"	-->\n"
"	<exec_method\n"
"		type='method'\n"
"		name='%s'\n"
"		%s=':kill'\n"
"		timeout_seconds='0'>\n";

static const char xml_exec_method_offline[] =
"	<!--\n"
"	    Use inetd's built-in process kill support to offline wait type\n"
"	    services.\n"
"	-->\n"
"	<exec_method\n"
"		type='method'\n"
"		name='%s'\n"
"		%s=':kill_process'\n"
"		timeout_seconds='0'>\n";

static const char xml_inetconv_group_start[] =
"	<!--\n"
"	    This property group is used to record information about\n"
"	    how this manifest was created.  It is an implementation\n"
"	    detail which should not be modified or deleted.\n"
"	-->\n"
"	<property_group name='%s' type='framework'>\n"
"		<propval name='%s' type='boolean' value='%s' />\n"
"		<propval name='%s' type='integer' value='%d' />\n"
"		<propval name='%s' type='astring' value=\n"
"'%s %s %s %s %s %s%s%s'\n"
"		/>\n";

static const char xml_property_group_start[] =
"	<property_group name='%s' type='framework'>\n"
"		<propval name='%s' type='astring' value='%s' />\n"
"		<propval name='%s' type='astring' value='%s' />\n"
"		<propval name='%s' type='astring' value='%s' />\n"
"		<propval name='%s' type='boolean' value='%s' />\n"
"		<propval name='%s' type='boolean' value='%s' />\n";

static const char xml_property_group_rpc[] =
"		<propval name='%s' type='integer' value='%d' />\n"
"		<propval name='%s' type='integer' value='%d' />"
"\n";

static const char xml_property_group_end[] =
"	</property_group>\n\n";

static const char xml_stability[] =
"	<stability value='External' />\n\n";

static const char xml_template[] =
"	<template>\n"
"		<common_name>\n"
"			<loctext xml:lang='C'>\n"
"%s\n"
"			</loctext>\n"
"		</common_name>\n"
"	</template>\n";

static const char xml_footer[] =
"</service>\n"
"\n"
"</service_bundle>\n";
/* end of manifest XML template strings */

static void *
safe_malloc(size_t size)
{
	void *cp;

	if ((cp = malloc(size)) == NULL) {
		(void) fprintf(stderr, gettext("%s: malloc failed: %s\n"),
		    progname, strerror(errno));
		exit(EXIT_ERROR_SYS);
	}
	return (cp);
}

static char *
safe_strdup(char *s)
{
	char *cp;

	if ((cp = strdup(s)) == NULL) {
		(void) fprintf(stderr, gettext("%s: strdup failed: %s\n"),
		    progname, strerror(errno));
		exit(EXIT_ERROR_SYS);
	}
	return (cp);
}

static char *
propertyname(char *name, char *prefix)
{
	static char *buf;
	size_t len;
	int c;
	char *cp;

	/* free any memory allocated by a previous call */
	free(buf);

	len = strlen(name) + strlen(prefix) + 1;
	buf = safe_malloc(len);
	buf[0] = '\0';

	/*
	 * Property names must match the regular expression:
	 * ([A-Za-z][_A-Za-z0-9.-]*,)?[A-Za-z][_A-Za-z0-9-]*
	 */

	/*
	 * Make sure the first character is alphabetic, if not insert prefix.
	 * Can't use isalpha() here as it's locale dependent but the property
	 * name regular expression isn't.
	 */
	c = name[0];
	if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) {
		(void) strlcat(buf, prefix, len);
	}
	(void) strlcat(buf, name, len);

	/* convert any disallowed characters into '_' */
	for (cp = buf; *cp != '\0'; cp++) {
		if ((*cp < 'A' || *cp > 'Z') && (*cp < 'a' || *cp > 'z') &&
		    (*cp < '0' || *cp > '9') && (*cp != '.') && (*cp != '-'))
			*cp = '_';
	}
	return (buf);
}

static char *
servicename(struct inetconfent *iconf)
{
	static char *buf;
	size_t len;
	char *cp, *proto;

	/* free any memory allocated by a previous call */
	free(buf);

	len = strlen(iconf->service) + strlen(iconf->protocol) +
	    sizeof ("rpc-/visible");
	buf = safe_malloc(len);

	/*
	 * Combine the service and protocol fields to produce a unique
	 * manifest service name. The syntax of a service name is:
	 * prop(/prop)*
	 */
	(void) strlcpy(buf, propertyname(iconf->service,
	    iconf->isrpc ? "rpc-": "s-"), len);
	(void) strlcat(buf, "/", len);

	proto = iconf->protocol;
	if (iconf->isrpc && (strcmp(iconf->protocol, "rpc/*") == 0))
		proto = "rpc/visible";

	/*
	 * SMF service names may not contain '.', but IANA services do
	 * allow its use, and property names can contain '.' as returned
	 * by propertyname().  So if the resultant SMF service name
	 * would contain a '.' we fix it here.
	 */
	for (cp = buf; *cp != '\0'; cp++) {
		if (*cp == '.')
			*cp = '_';
	}
	(void) strlcat(buf, propertyname(proto, "p-"), len);
	return (buf);
}

static boolean_t
is_v6only(char *protocol)
{
	/* returns true if protocol is an IPv6 only protocol */
	if ((strcmp(protocol, SOCKET_PROTO_TCP6_ONLY) == 0) ||
	    (strcmp(protocol, SOCKET_PROTO_UDP6_ONLY) == 0))
		return (B_TRUE);
	return (B_FALSE);
}

static char *
invalid_props(inetd_prop_t *p)
{
	static char
	    buf[sizeof (" service-name endpoint-type protocol wait-status")];

	buf[0] = '\0';
	if ((p[PT_SVC_NAME_INDEX].ip_error == IVE_INVALID) ||
	    (p[PT_SVC_NAME_INDEX].ip_error == IVE_UNSET) ||
	    (p[PT_RPC_LW_VER_INDEX].ip_error == IVE_INVALID) ||
	    (p[PT_RPC_HI_VER_INDEX].ip_error == IVE_INVALID))
		(void) strlcat(buf, " service-name", sizeof (buf));
	if ((p[PT_SOCK_TYPE_INDEX].ip_error == IVE_INVALID) ||
	    (p[PT_SOCK_TYPE_INDEX].ip_error == IVE_UNSET))
		(void) strlcat(buf, " endpoint-type", sizeof (buf));
	if ((p[PT_PROTO_INDEX].ip_error == IVE_INVALID) ||
	    (p[PT_PROTO_INDEX].ip_error == IVE_UNSET) ||
	    (p[PT_ISRPC_INDEX].ip_error == IVE_INVALID))
		(void) strlcat(buf, " protocol", sizeof (buf));
	if (p[PT_ISWAIT_INDEX].ip_error == IVE_INVALID)
		(void) strlcat(buf, " wait-status", sizeof (buf));
	return (buf);
}

static boolean_t
valid_basic_properties(struct inetconfent *iconf, struct fileinfo *finfo)
{
	size_t prop_size;
	inetd_prop_t *prop, *inetd_properties;
	boolean_t valid = B_TRUE;
	char *proto = iconf->protocol;
	char *svc_name = iconf->service;

	inetd_properties = get_prop_table(&prop_size);
	prop = safe_malloc(prop_size * sizeof (inetd_prop_t));
	(void) memcpy(prop, inetd_properties,
	    prop_size * sizeof (inetd_prop_t));

	put_prop_value_boolean(prop, PR_ISRPC_NAME, iconf->isrpc);
	put_prop_value_boolean(prop, PR_ISWAIT_NAME, iconf->wait);
	if (iconf->isrpc) {
		put_prop_value_int(prop, PR_RPC_LW_VER_NAME,
		    iconf->rpc_low_version);
		put_prop_value_int(prop, PR_RPC_HI_VER_NAME,
		    iconf->rpc_high_version);
		svc_name = iconf->rpc_prog;
		proto += 4;	/* skip 'rpc/' */
	}

	if (!put_prop_value_string(prop, PR_SOCK_TYPE_NAME, iconf->endpoint) ||
	    !put_prop_value_string(prop, PR_SVC_NAME_NAME, svc_name)) {
		valid = B_FALSE;

		if (errno == ENOMEM) {
			(void) fprintf(stderr,
			    gettext("%s: failed to allocate memory: %s\n"),
			    progname, strerror(errno));
			exit(EXIT_ERROR_SYS);
		}
	}

	put_prop_value_string_list(prop, PR_PROTO_NAME, get_protos(proto));

	if (!valid_props(prop, NULL, NULL, NULL, NULL) || !valid) {
		valid = B_FALSE;
		(void) fprintf(stderr, gettext("%s: Error %s line %d "
		    "invalid or inconsistent fields:%s\n"), progname,
		    finfo->filename, finfo->lineno,
		    invalid_props(prop));
	}

	free_instance_props(prop);
	return (valid);
}

static boolean_t
valid_inetconfent(struct inetconfent *iconf, struct fileinfo *finfo)
{
	boolean_t valid = B_TRUE;
	size_t len;
	char *cp, *endp;
	struct passwd *pwd;
	struct group *grp;
	struct stat statb;
	char *proto = iconf->protocol;

	iconf->isrpc = B_FALSE;
	if (strncmp(iconf->protocol, "rpc/", 4) == 0) {
		iconf->isrpc = B_TRUE;
		iconf->rpc_prog = safe_strdup(iconf->service);

		/* set RPC version numbers */
		iconf->rpc_low_version = 1;
		iconf->rpc_high_version = 1;
		if ((cp = strrchr(iconf->rpc_prog, '/')) != NULL) {
			*cp = '\0';
			if (*++cp != '\0') {
				errno = 0;
				iconf->rpc_low_version = strtol(cp, &endp, 10);
				if (errno != 0)
					goto vererr;
				cp = endp;
				if (*cp == '-') {
					if (*++cp == '\0')
						goto vererr;
					errno = 0;
					iconf->rpc_high_version = strtol(cp,
					    &endp, 10);
					if ((errno != 0) || (*endp != '\0'))
						goto vererr;
				} else if (*cp == '\0') {
					iconf->rpc_high_version =
					    iconf->rpc_low_version;
				} else {
vererr:
					(void) fprintf(stderr, gettext(
					    "%s: Error %s line %d invalid RPC "
					    "version in service: %s\n"),
					    progname, finfo->filename,
					    finfo->lineno, iconf->service);
					valid = B_FALSE;
				}
			}
		}
		proto += 4;	/* skip 'rpc/' */
	}
	/* tcp6only and udp6only are not valid in inetd.conf */
	if (is_v6only(proto)) {
		(void) fprintf(stderr, gettext("%s: Error %s line %d "
		    "invalid protocol: %s\n"), progname,
		    finfo->filename, finfo->lineno, proto);
		valid = B_FALSE;
	}

	if (strcmp(iconf->wait_status, "wait") == 0) {
		iconf->wait = B_TRUE;
	} else if (strcmp(iconf->wait_status, "nowait") == 0) {
		iconf->wait = B_FALSE;
	} else {
		(void) fprintf(stderr,
		    gettext("%s: Error %s line %d invalid wait-status: %s\n"),
		    progname, finfo->filename, finfo->lineno,
		    iconf->wait_status);
		valid = B_FALSE;
	}

	/* look up the username to set the groupname */
	if ((pwd = getpwnam(iconf->username)) == NULL) {
		(void) fprintf(stderr,
		    gettext("%s: Error %s line %d unknown user: %s\n"),
		    progname, finfo->filename, finfo->lineno,
		    iconf->username);
		valid = B_FALSE;
	} else {
		if ((grp = getgrgid(pwd->pw_gid)) != NULL) {
			iconf->groupname = safe_strdup(grp->gr_name);
		} else {
			/* use the group ID if no groupname */
			char s[1];

			len = snprintf(s, 1, "%d", pwd->pw_gid) + 1;
			iconf->groupname = safe_malloc(len);
			(void) snprintf(iconf->groupname, len, "%d",
			    pwd->pw_gid);
		}
	}

	/* check for internal services */
	if (strcmp(iconf->server_program, "internal") == 0) {
		valid = B_FALSE;
		if ((strcmp(iconf->service, "echo") == 0) ||
		    (strcmp(iconf->service, "discard") == 0) ||
		    (strcmp(iconf->service, "time") == 0) ||
		    (strcmp(iconf->service, "daytime") == 0) ||
		    (strcmp(iconf->service, "chargen") == 0)) {
			(void) fprintf(stderr, gettext(
			    "%s: Error %s line %d the SUNWcnsr and SUNWcnsu"
			    " packages contain the internal services\n"),
			    progname, finfo->filename, finfo->lineno);
		} else {
			(void) fprintf(stderr, gettext("%s: Error %s line %d "
			    "unknown internal service: %s\n"), progname,
			    finfo->filename, finfo->lineno, iconf->service);
		}
	} else if ((stat(iconf->server_program, &statb) == -1) &&
	    (errno == ENOENT)) {
		(void) fprintf(stderr, gettext(
		    "%s: Error %s line %d server-program not found: %s\n"),
		    progname, finfo->filename, finfo->lineno,
		    iconf->server_program);
		valid = B_FALSE;
	}

	return (valid && valid_basic_properties(iconf, finfo));
}

static void
free_inetconfent(struct inetconfent *iconf)
{
	if (iconf == NULL)
		return;

	free(iconf->service);
	free(iconf->endpoint);
	free(iconf->protocol);
	free(iconf->wait_status);
	free(iconf->username);
	free(iconf->server_program);
	free(iconf->server_args);
	free(iconf->rpc_prog);
	free(iconf->groupname);
	free(iconf->exec);
	free(iconf->arg0);

	free(iconf);
}

static struct inetconfent *
line_to_inetconfent(char *line)
{
	char *cp;
	struct inetconfent *iconf;

	iconf = safe_malloc(sizeof (struct inetconfent));
	(void) memset(iconf, 0, sizeof (struct inetconfent));

	if ((cp = strtok(line, " \t\n")) == NULL)
		goto fail;
	iconf->service = safe_strdup(cp);

	if ((cp = strtok(NULL, " \t\n")) == NULL)
		goto fail;
	iconf->endpoint = safe_strdup(cp);

	if ((cp = strtok(NULL, " \t\n")) == NULL)
		goto fail;
	iconf->protocol = safe_strdup(cp);

	if ((cp = strtok(NULL, " \t\n")) == NULL)
		goto fail;
	iconf->wait_status = safe_strdup(cp);

	if ((cp = strtok(NULL, " \t\n")) == NULL)
		goto fail;
	iconf->username = safe_strdup(cp);

	if ((cp = strtok(NULL, " \t\n")) == NULL)
		goto fail;
	iconf->server_program = safe_strdup(cp);

	/* last field is optional */
	if ((cp = strtok(NULL, "\n")) != NULL)
		iconf->server_args = safe_strdup(cp);

	/* Combine args and server name to construct exec and args fields */
	if (iconf->server_args == NULL) {
		iconf->exec = safe_strdup(iconf->server_program);
	} else {
		int len;
		char *args, *endp;

		len = strlen(iconf->server_program) +
		    strlen(iconf->server_args) + 1;
		iconf->exec = safe_malloc(len);
		(void) strlcpy(iconf->exec, iconf->server_program, len);

		args = safe_strdup(iconf->server_args);
		if ((cp = strtok(args, " \t")) != NULL) {
			if ((endp = strrchr(iconf->exec, '/')) == NULL)
				endp = iconf->exec;
			else
				endp++;
			/* only set arg0 property value if needed */
			if (strcmp(endp, cp) != 0)
				iconf->arg0 = safe_strdup(cp);
			while ((cp = strtok(NULL, " \t")) != NULL) {
				(void) strlcat(iconf->exec, " ", len);
				(void) strlcat(iconf->exec, cp, len);
			}
		}
		free(args);
	}

	return (iconf);
fail:
	free_inetconfent(iconf);
	return (NULL);
}

static void
skipline(FILE *fp)
{
	int c;

	/* skip remainder of a line */
	while (((c = getc(fp)) != EOF) && (c != '\n'))
		;
}

static struct inetconfent *
fgetinetconfent(struct fileinfo *finfo, boolean_t validate)
{
	char *cp;
	struct inetconfent *iconf;
	char line[MAX_SRC_LINELEN];

	while (fgets(line, sizeof (line), finfo->fp) != NULL) {
		finfo->lineno++;

		/* skip empty or commented out lines */
		if (*line == '\n')
			continue;
		if (*line == '#') {
			if (line[strlen(line) - 1] != '\n')
				skipline(finfo->fp);
			continue;
		}
		/* check for lines which are too long */
		if (line[strlen(line) - 1] != '\n') {
			(void) fprintf(stderr,
			    gettext("%s: Error %s line %d too long, skipped\n"),
			    progname, finfo->filename, finfo->lineno);
			skipline(finfo->fp);
			finfo->failcnt++;
			continue;
		}
		/* remove in line comments and newline character */
		if ((cp = strchr(line, '#')) == NULL)
			cp = strchr(line, '\n');
		if (cp)
			*cp = '\0';

		if ((iconf = line_to_inetconfent(line)) == NULL) {
			(void) fprintf(stderr, gettext(
			    "%s: Error %s line %d too few fields, skipped\n"),
			    progname, finfo->filename, finfo->lineno);
			finfo->failcnt++;
			continue;
		}

		if (!validate || valid_inetconfent(iconf, finfo))
			return (iconf);

		finfo->failcnt++;
		free_inetconfent(iconf);
	}
	return (NULL);
}

static char *
boolstr(boolean_t val)
{
	if (val)
		return ("true");
	return ("false");
}

static int
print_manifest(FILE *f, char *filename, struct inetconfent *iconf)
{
	if (fprintf(f, xml_header) < 0)
		goto print_err;

	if (fprintf(f, xml_comment,
	    iconf->isrpc ? iconf->rpc_prog : iconf->service) < 0)
		goto print_err;

	if (fprintf(f, xml_service_bundle, iconf->service) < 0)
		goto print_err;
	if (fprintf(f, xml_service_name, servicename(iconf)) < 0)
		goto print_err;
	if (fprintf(f, xml_instance) < 0)
		goto print_err;
	if (fprintf(f, xml_restarter, INETD_INSTANCE_FMRI) < 0)
		goto print_err;
	if (iconf->isrpc) {
		if (fprintf(f, xml_dependency, "rpcbind", RPCBIND_FMRI) < 0)
			goto print_err;
	}

	if (fprintf(f, xml_exec_method_start, START_METHOD_NAME, PR_EXEC_NAME,
	    iconf->exec, PR_USER_NAME, iconf->username, iconf->groupname) < 0)
		goto print_err;
	if (iconf->arg0 != NULL) {
		if (fprintf(f, xml_arg0, PR_ARG0_NAME, iconf->arg0) < 0)
			goto print_err;
	}
	if (fprintf(f, xml_exec_method_end) < 0)
		goto print_err;

	if (fprintf(f, xml_exec_method_disable, DISABLE_METHOD_NAME,
	    PR_EXEC_NAME) < 0)
		goto print_err;
	if (fprintf(f, xml_exec_method_end) < 0)
		goto print_err;

	if (iconf->wait) {
		if (fprintf(f, xml_exec_method_offline, OFFLINE_METHOD_NAME,
		    PR_EXEC_NAME) < 0)
			goto print_err;
		if (fprintf(f, xml_exec_method_end) < 0)
			goto print_err;
	}

	if (fprintf(f, xml_inetconv_group_start, PG_NAME_INETCONV,
	    PR_AUTO_CONVERTED_NAME, boolstr(B_TRUE),
	    PR_VERSION_NAME, INETCONV_VERSION,
	    PR_SOURCE_LINE_NAME, iconf->service,
	    iconf->endpoint, iconf->protocol, iconf->wait_status,
	    iconf->username, iconf->server_program,
	    iconf->server_args == NULL ? "" : " ",
	    iconf->server_args == NULL ? "" : iconf->server_args) < 0)
		goto print_err;
	if (fprintf(f, xml_property_group_end) < 0)
		goto print_err;

	if (fprintf(f, xml_property_group_start, PG_NAME_SERVICE_CONFIG,
	    PR_SVC_NAME_NAME, iconf->isrpc ? iconf->rpc_prog : iconf->service,
	    PR_SOCK_TYPE_NAME, iconf->endpoint,
	    PR_PROTO_NAME, iconf->isrpc ? iconf->protocol + 4 :
	    iconf->protocol,
	    PR_ISWAIT_NAME, boolstr(iconf->wait),
	    PR_ISRPC_NAME, boolstr(iconf->isrpc)) < 0)
		goto print_err;
	if (iconf->isrpc) {
		if (fprintf(f, xml_property_group_rpc,
		    PR_RPC_LW_VER_NAME, iconf->rpc_low_version,
		    PR_RPC_HI_VER_NAME, iconf->rpc_high_version) < 0)
			goto print_err;
	}
	if (fprintf(f, xml_property_group_end) < 0)
		goto print_err;

	if (fprintf(f, xml_stability) < 0)
		goto print_err;
	if (fprintf(f, xml_template,
	    iconf->isrpc ? iconf->rpc_prog : iconf->service) < 0)
		goto print_err;
	if (fprintf(f, xml_footer) < 0)
		goto print_err;

	(void) printf("%s -> %s\n", iconf->service, filename);
	return (0);

print_err:
	(void) fprintf(stderr, gettext("%s: Error writing manifest %s: %s\n"),
	    progname, filename, strerror(errno));
	return (-1);
}

static struct fileinfo *
open_srcfile(char *filename)
{
	struct fileinfo *finfo = NULL;
	FILE *fp;

	if (filename != NULL) {
		if ((fp = fopen(filename, "r")) == NULL) {
			(void) fprintf(stderr,
			    gettext("%s: Error opening %s: %s\n"),
			    progname, filename, strerror(errno));
		}
	} else {
		/*
		 * If no source file specified, do the same as inetd and first
		 * try /etc/inet/inetd.conf, followed by /etc/inetd.conf.
		 */
		filename = MAIN_CONFIG;
		if ((fp = fopen(filename, "r")) == NULL) {
			(void) fprintf(stderr,
			    gettext("%s: Error opening %s: %s\n"),
			    progname, filename, strerror(errno));
			filename = ALT_CONFIG;
			if ((fp = fopen(filename, "r")) == NULL) {
				(void) fprintf(stderr, gettext(
				    "%s: Error opening %s: %s\n"), progname,
				    filename, strerror(errno));
			}
		}
	}
	if (fp != NULL) {
		finfo = safe_malloc(sizeof (struct fileinfo));
		finfo->fp = fp;
		finfo->filename = filename;
		finfo->lineno = 0;
		finfo->failcnt = 0;
		(void) fcntl(fileno(fp), F_SETFD, FD_CLOEXEC);
	}
	return (finfo);
}

/*
 * Opens manifest output file.  Returns 0 on success, -1 if the file
 * exists, -2 on other errors.
 */
static int
open_dstfile(
    char *destdir,
    boolean_t overwrite,
    struct inetconfent *iconf,
    struct fileinfo **finfo)
{
	int fd;
	size_t len;
	char *dstfile, *cp, *proto;
	FILE *fp;

	/* if no destdir specified, use appropriate default */
	if (destdir == NULL) {
		if (iconf->isrpc)
			destdir = MANIFEST_RPC_DIR;
		else
			destdir = MANIFEST_DIR;
	}

	len = strlen(destdir) + strlen(iconf->service) +
	    strlen(iconf->protocol) + sizeof ("/-visible.xml");
	dstfile = safe_malloc(len);

	(void) strlcpy(dstfile, destdir, len);
	if (dstfile[strlen(dstfile) - 1] != '/')
		(void) strlcat(dstfile, "/", len);
	cp = dstfile + strlen(dstfile);

	(void) strlcat(dstfile, iconf->service, len);
	(void) strlcat(dstfile, "-", len);

	proto = iconf->protocol;
	if (iconf->isrpc && (strcmp(iconf->protocol, "rpc/*") == 0))
		proto = "rpc/visible";

	(void) strlcat(dstfile, proto, len);
	(void) strlcat(dstfile, ".xml", len);

	/* convert any '/' chars in service or protocol to '_' chars */
	while ((cp = strchr(cp, '/')) != NULL)
		*cp = '_';

	fd = open(dstfile, O_WRONLY|O_CREAT|(overwrite ? O_TRUNC : O_EXCL),
	    0644);
	if (fd == -1) {
		if (!overwrite && (errno == EEXIST)) {
			(void) fprintf(stderr,
			    gettext("%s: Notice: Service manifest for "
			    "%s already generated as %s, skipped\n"),
			    progname, iconf->service, dstfile);
			free(dstfile);
			return (-1);
		} else {
			(void) fprintf(stderr,
			    gettext("%s: Error opening %s: %s\n"),
			    progname, dstfile, strerror(errno));
			free(dstfile);
			return (-2);
		}
	}
	/* Clear errno to catch the "no stdio streams" case */
	errno = 0;
	if ((fp = fdopen(fd, "w")) == NULL) {
		char *s = strerror(errno);
		if (errno == 0)
			s = gettext("No stdio streams available");
		(void) fprintf(stderr, gettext("%s: Error fdopen failed: %s\n"),
		    progname, s);
		(void) close(fd);
		free(dstfile);
		return (-2);
	}
	*finfo = safe_malloc(sizeof (struct fileinfo));
	(*finfo)->fp = fp;
	(*finfo)->filename = dstfile;
	(*finfo)->lineno = 0;
	(*finfo)->failcnt = 0;
	return (0);
}

static int
import_manifest(char *filename)
{
	int status;
	pid_t pid, wpid;
	char *cp;

	if ((cp = strrchr(filename, '/')) == NULL)
		cp = filename;
	else
		cp++;
	(void) printf(gettext("Importing %s ..."), cp);

	if ((pid = fork()) == -1) {
		(void) fprintf(stderr,
		    gettext("\n%s: fork failed, %s not imported: %s\n"),
		    progname, filename, strerror(errno));
		exit(EXIT_ERROR_SYS);
	}
	if (pid == 0) {
		/* child */
		(void) fclose(stdin);
		(void) setenv("SVCCFG_CHECKHASH", "1", 1);
		(void) execl(SVCCFG_PATH, "svccfg", "import", filename, NULL);
		(void) fprintf(stderr, gettext("\n%s: exec of %s failed: %s"),
		    progname, SVCCFG_PATH, strerror(errno));
		_exit(EXIT_ERROR_SYS);
	}
	/* parent */
	if ((wpid = waitpid(pid, &status, 0)) != pid) {
		(void) fprintf(stderr, gettext(
		    "\n%s: unexpected wait (%d) from import of %s: %s\n"),
		    progname, wpid, filename, strerror(errno));
		return (-1);
	}
	if (WIFEXITED(status) && (WEXITSTATUS(status) != 0)) {
		(void) fprintf(stderr,
		    gettext("\n%s: import failure (%d) for %s\n"),
		    progname, WEXITSTATUS(status), filename);
		return (-1);
	}
	(void) printf(gettext("Done\n"));
	return (0);
}

static int
inetd_config_path(char **path)
{
	int fd;
	char *arg1, *configfile, *configstr;
	scf_simple_prop_t *sp;
	char cpath[PATH_MAX];

	if ((sp = scf_simple_prop_get(NULL, INETD_INSTANCE_FMRI, "start",
	    SCF_PROPERTY_EXEC)) == NULL)
		return (-1);
	if ((configstr = scf_simple_prop_next_astring(sp)) == NULL) {
		scf_simple_prop_free(sp);
		return (-1);
	}
	configstr = safe_strdup(configstr);
	scf_simple_prop_free(sp);

	/*
	 * Look for the optional configuration file, the syntax is:
	 * /usr/lib/inet/inetd [config-file] start|stop|refresh|disable|%m
	 */
	if (strtok(configstr, " \t") == NULL) {
		free(configstr);
		return (-1);
	}
	if ((arg1 = strtok(NULL, " \t")) == NULL) {
		free(configstr);
		return (-1);
	}
	if (strtok(NULL, " \t") == NULL) {
		/*
		 * No configuration file specified, do the same as inetd and
		 * first try /etc/inet/inetd.conf, followed by /etc/inetd.conf.
		 */
		configfile = MAIN_CONFIG;
		if ((fd = open(configfile, O_RDONLY)) >= 0)
			(void) close(fd);
		else
			configfile = ALT_CONFIG;

	} else {
		/* make sure there are no more arguments */
		if (strtok(NULL, " \t") != NULL) {
			free(configstr);
			return (-1);
		}
		configfile = arg1;
	}

	/* configuration file must be an absolute pathname */
	if (*configfile != '/') {
		free(configstr);
		return (-1);
	}

	if (realpath(configfile, cpath) == NULL)
		(void) strlcpy(cpath, configfile, sizeof (cpath));

	free(configstr);
	*path = safe_strdup(cpath);
	return (0);
}

static int
update_hash(char *srcfile)
{
	scf_error_t rval;
	char *inetd_cpath, *hashstr;
	char cpath[PATH_MAX];

	/* determine the config file inetd is using */
	if (inetd_config_path(&inetd_cpath) == -1) {
		(void) fprintf(stderr,
		    gettext("%s: Error reading from repository\n"), progname);
		return (-1);
	}

	/* resolve inetconv input filename */
	if (realpath(srcfile, cpath) == NULL)
		(void) strlcpy(cpath, srcfile, sizeof (cpath));

	/* if inetconv and inetd are using the same config file, update hash */
	if (strcmp(cpath, inetd_cpath) != 0) {
		free(inetd_cpath);
		return (0);
	}
	free(inetd_cpath);

	/* generic error message as use of hash is not exposed to the user */
	if (calculate_hash(cpath, &hashstr) != 0) {
		(void) fprintf(stderr,
		    gettext("%s: Error unable to update repository\n"),
		    progname);
		return (-1);
	}
	/* generic error message as use of hash is not exposed to the user */
	if ((rval = store_inetd_hash(hashstr)) != SCF_ERROR_NONE) {
		(void) fprintf(stderr,
		    gettext("%s: Error updating repository: %s\n"),
		    progname, scf_strerror(rval));
		free(hashstr);
		return (-1);
	}
	free(hashstr);
	return (0);
}

static void
property_error(const char *fmri, const char *prop)
{
	(void) fprintf(stderr,
	    gettext("Error: Instance %1$s is missing property '%2$s'.\n"),
	    fmri, prop);
}

/*
 * modify_sprop takes a handle, an instance, a property group, a property,
 * and an astring value, and modifies the instance (or service's) specified
 * property in the repository to the submitted value.
 *
 * returns -1 on error, 1 on successful transaction completion.
 */

static int
modify_sprop(scf_handle_t *h, const scf_instance_t *inst,
    const char *pg, const char *prop, const char *value)
{
	scf_transaction_t		*tx = NULL;
	scf_transaction_entry_t		*ent = NULL;
	scf_propertygroup_t		*gpg = NULL;
	scf_property_t			*eprop = NULL;
	scf_value_t			*v = NULL;
	scf_service_t			*svc = NULL;
	int				ret = 0, create = 0;

	if ((gpg = scf_pg_create(h)) == NULL)
		return (-1);

	/* Get the property group */
	if (scf_instance_get_pg(inst, pg, gpg) == -1) {
		/* Not a property of the instance, try the service instead */
		if ((svc = scf_service_create(h)) == NULL) {
			ret = -1;
			goto out;
		}
		if ((scf_instance_get_parent(inst, svc) == -1) ||
		    (scf_service_get_pg(svc, pg, gpg) == -1)) {
			ret = -1;
			goto out;
		}
	}

	if ((eprop = scf_property_create(h)) == NULL) {
		ret = -1;
		goto out;
	}

	if (scf_pg_get_property(gpg, prop, eprop) == -1) {
		if (scf_error() != SCF_ERROR_NOT_FOUND) {
			ret = -1;
			goto out;
		}

		create = 1;
	}

	if ((tx = scf_transaction_create(h)) == NULL ||
	    (ent = scf_entry_create(h)) == NULL) {
		ret = -1;
		goto out;
	}

	do {
		if (scf_transaction_start(tx, gpg) == -1) {
			ret = -1;
			goto out;
		}

		/* Modify the property */
		if (create)
			ret = scf_transaction_property_new(tx, ent, prop,
			    SCF_TYPE_ASTRING);
		else
			ret = scf_transaction_property_change_type(tx, ent,
			    prop, SCF_TYPE_ASTRING);

		if (ret == -1)
			goto out;

		if ((v = scf_value_create(h)) == NULL) {
			ret = -1;
			goto out;
		}

		if (scf_value_set_astring(v, value) == -1) {
			ret = -1;
			goto out;
		}

		if (scf_entry_add_value(ent, v) == -1) {
			ret = -1;
			goto out;
		}

		ret = scf_transaction_commit(tx);

		if (ret == 0) {
			/* Property group was stale, retry */
			if (scf_pg_update(gpg) == -1) {
				ret = -1;
				goto out;
			}
			scf_transaction_reset(tx);
		}

	} while (ret == 0);
out:
	scf_value_destroy(v);
	scf_entry_destroy(ent);
	scf_transaction_destroy(tx);
	scf_property_destroy(eprop);
	scf_service_destroy(svc);
	scf_pg_destroy(gpg);

	return (ret);
}

/*
 * list_callback is the callback function to be handed to simple_walk_instances
 * in main.  It is called once on every instance on a machine.  If that
 * instance is controlled by inetd, we test whether it's the same
 * service that we're looking at from the inetd.conf file, and enable it if
 * they are the same.
 */

/*ARGSUSED*/
static int
list_callback(scf_handle_t *h, scf_instance_t *inst, void *buf)
{
	ssize_t			max_name_length;
	char			*svc_name;
	scf_simple_prop_t	*prop = NULL;
	scf_simple_prop_t	*sockprop = NULL;
	scf_simple_prop_t	*rpcprop = NULL;
	scf_simple_prop_t	*progprop = NULL;
	const char		*name, *endpoint, *restart_str, *prog;
	struct inetconfent	*iconf = (struct inetconfent *)buf;
	uint8_t			*isrpc;

	max_name_length = scf_limit(SCF_LIMIT_MAX_NAME_LENGTH);
	if ((svc_name = malloc(max_name_length + 1)) == NULL) {
		(void) fprintf(stderr, gettext("Error: Out of memory.\n"));
		return (SCF_FAILED);
	}

	/*
	 * Get the FMRI of the instance, and check if its delegated restarter
	 * is inetd.  A missing or empty restarter property implies that
	 * svc.startd is the restarter.
	 */

	if (scf_instance_to_fmri(inst, svc_name, max_name_length) < 0) {
		(void) fprintf(stderr,
		    gettext("Error: Unable to obtain FMRI for service %1$s."),
		    svc_name);
		free(svc_name);
		return (SCF_FAILED);
	}

	if ((prop = scf_simple_prop_get(h, svc_name, SCF_PG_GENERAL,
	    SCF_PROPERTY_RESTARTER)) == NULL)
		goto out;

	if ((restart_str = scf_simple_prop_next_ustring(prop)) == NULL)
		goto out;

	if (strcmp(restart_str, INETD_INSTANCE_FMRI) != 0)
		goto out;

	/* Free restarter prop so it can be reused below */
	scf_simple_prop_free(prop);

	/*
	 * We know that this instance is managed by inetd.
	 * Now get the properties needed to decide if it matches this
	 * line in the old config file.
	 */

	if (((prop = scf_simple_prop_get(h, svc_name, PG_NAME_SERVICE_CONFIG,
	    PR_SVC_NAME_NAME)) == NULL) ||
	    ((name = scf_simple_prop_next_astring(prop)) == NULL)) {
		property_error(svc_name, PR_SVC_NAME_NAME);
		goto out;
	}

	if (((sockprop = scf_simple_prop_get(h, svc_name,
	    PG_NAME_SERVICE_CONFIG, PR_SOCK_TYPE_NAME)) == NULL) ||
	    ((endpoint = scf_simple_prop_next_astring(sockprop)) == NULL)) {
		property_error(svc_name, PR_SOCK_TYPE_NAME);
		goto out;
	}

	if (((rpcprop = scf_simple_prop_get(h, svc_name,
	    PG_NAME_SERVICE_CONFIG, PR_ISRPC_NAME)) == NULL) ||
	    ((isrpc = scf_simple_prop_next_boolean(rpcprop)) == NULL)) {
		property_error(svc_name, PR_ISRPC_NAME);
		goto out;
	}

	if (((progprop = scf_simple_prop_get(h, svc_name, START_METHOD_NAME,
	    PR_EXEC_NAME)) == NULL) ||
	    ((prog = scf_simple_prop_next_astring(progprop)) == NULL)) {
		property_error(svc_name, PR_EXEC_NAME);
	}


	/* If it's RPC, we truncate off the version portion for comparison */
	if (*isrpc) {
		char *cp;

		cp = strchr(iconf->service, '/');
		if (cp != NULL)
			*cp = '\0';
	}

	/*
	 * If name of this service and endpoint are equal to values from
	 * iconf fields, and they're either both RPC or both non-RPC,
	 * then we have a match; update the exec and arg0 properties if
	 * necessary, then enable it.
	 * We don't return an error if either operation fails so that we
	 * continue to try all the other services.
	 */
	if (strcmp(name, iconf->service) == 0 &&
	    strcmp(endpoint, iconf->endpoint) == 0 &&
	    *isrpc == (strncmp(iconf->protocol, "rpc/", 4) == 0)) {
		/* Can't update exec on internal services */
		if ((strcmp(iconf->server_program, "internal") != 0) &&
		    (strcmp(iconf->exec, prog) != 0)) {
			/* User had edited the command */
			if (!import) {
				/* Dry run only */
				(void) printf(
				    gettext("Would update %s to %s %s"),
				    svc_name, PR_EXEC_NAME, iconf->exec);
				if (iconf->arg0 != NULL) {
					(void) printf(
					    gettext(" with %s of %s\n"),
					    PR_ARG0_NAME, iconf->arg0);
				} else {
					(void) printf("\n");
				}
			} else {
				/* Update instance's exec property */
				if (modify_sprop(h, inst, START_METHOD_NAME,
				    PR_EXEC_NAME, iconf->exec) != 1)
					(void) fprintf(stderr,
					    gettext("Error: Unable to update "
					    "%s property of %s, %s\n"),
					    PR_EXEC_NAME, svc_name,
					    scf_strerror(scf_error()));
				else
					(void) printf("%s will %s %s\n",
					    svc_name, PR_EXEC_NAME,
					    iconf->exec);

				/* Update arg0 prop, if needed */
				if (iconf->arg0 != NULL) {
					if (modify_sprop(h, inst,
					    START_METHOD_NAME, PR_ARG0_NAME,
					    iconf->arg0) != 1) {
						(void) fprintf(stderr,
						    gettext("Error: Unable to "
						    "update %s property of "
						    "%s, %s\n"), PR_ARG0_NAME,
						    svc_name,
						    scf_strerror(scf_error()));
					} else {
						(void) printf("%s will have an "
						    "%s of %s\n", svc_name,
						    PR_ARG0_NAME, iconf->arg0);
					}
				}
			}
		}

		if (!import) {
			/* Dry-run only */
			(void) printf("Would enable %s\n", svc_name);
		} else {
			if (smf_enable_instance(svc_name, 0) != 0)
				(void) fprintf(stderr,
				    gettext("Error: Failed to enable %s\n"),
				    svc_name);
			else
				(void) printf("%s enabled\n", svc_name);
		}
	}

out:
	free(svc_name);
	scf_simple_prop_free(prop);
	scf_simple_prop_free(sockprop);
	scf_simple_prop_free(rpcprop);
	scf_simple_prop_free(progprop);
	return (SCF_SUCCESS);
}

static void
usage(void)
{
	(void) fprintf(stderr, gettext(
	    "Usage: %1$s [-fn] [-i srcfile] [-o destdir]\n"
	    "       %1$s -e [-n] [-i srcfile]\n"
	    "-?          Display this usage message\n"
	    "-e          Enable smf services which are enabled in the input\n"
	    "            file\n"
	    "-f          Force overwrite of existing manifests\n"
	    "-n          Do not import converted manifests,\n"
	    "            or only display services which would be enabled\n"
	    "-i srcfile  Alternate input file\n"
	    "-o destdir  Alternate output directory for manifests\n"),
	    progname);
	exit(EXIT_USAGE);
}

int
main(int argc, char *argv[])
{
	int c, rval, convert_err, import_err = 0, enable_err = 0;
	boolean_t overwrite = B_FALSE;
	boolean_t enable = B_FALSE;
	char *srcfile = NULL;
	char *destdir = NULL;
	struct fileinfo *srcfinfo, *dstfinfo;
	struct inetconfent *iconf;

	setbuf(stdout, NULL);
	(void) setlocale(LC_ALL, "");
	(void) textdomain(TEXT_DOMAIN);

	if ((progname = strrchr(argv[0], '/')) == NULL)
		progname = argv[0];
	else
		progname++;

	while ((c = getopt(argc, argv, "?efni:o:")) != -1) {
		switch (c) {
		case 'e':
			/* enable services based on existing file config */
			enable = B_TRUE;
			break;

		case 'f':
			/* overwrite existing manifests */
			overwrite = B_TRUE;
			break;
		case 'n':
			/* don't import manifests, or dry-run enable */
			import = B_FALSE;
			break;
		case 'i':
			/* alternate input file */
			if (srcfile != NULL) {
				(void) fprintf(stderr,
				    gettext("%s: Error only one -%c allowed\n"),
				    progname, optopt);
				usage();
			}
			srcfile = optarg;
			break;
		case 'o':
			/* alternate output directory */
			if (destdir != NULL) {
				(void) fprintf(stderr,
				    gettext("%s: Error only one -%c allowed\n"),
				    progname, optopt);
				usage();
			}
			destdir = optarg;
			break;
		case '?': /*FALLTHROUGH*/
		default:
			usage();
			break;
		}
	}

	/*
	 * Display usage if extraneous args supplied or enable specified in
	 * combination with overwrite or destdir
	 */
	if ((optind != argc) || (enable && (overwrite || destdir != NULL)))
		usage();

	if ((srcfinfo = open_srcfile(srcfile)) == NULL)
		return (EXIT_ERROR_CONV);

	while ((iconf = fgetinetconfent(srcfinfo, !enable)) != NULL) {
		/*
		 * If we're enabling, then just walk all the services for each
		 * line and enable those which match.
		 */
		if (enable) {
			rval = scf_simple_walk_instances(SCF_STATE_ALL, iconf,
			    list_callback);
			free_inetconfent(iconf);
			if (rval == SCF_FAILED) {
				/* Only print msg if framework error */
				if (scf_error() != SCF_ERROR_CALLBACK_FAILED)
					(void) fprintf(stderr, gettext(
					    "Error walking instances: %s.\n"),
					    scf_strerror(scf_error()));
				enable_err++;
				break;
			}
			continue;
		}

		/* Remainder of loop used for conversion & import */
		if ((rval = open_dstfile(destdir, overwrite, iconf, &dstfinfo))
		    < 0) {
			/*
			 * Only increment error counter if the failure was
			 * other than the file already existing.
			 */
			if (rval == -2)
				srcfinfo->failcnt++;
			free_inetconfent(iconf);
			continue;
		}
		rval = print_manifest(dstfinfo->fp, dstfinfo->filename, iconf);
		(void) fclose(dstfinfo->fp);
		if (rval == 0) {
			if (import &&
			    (import_manifest(dstfinfo->filename) != 0))
				import_err++;
		} else {
			(void) unlink(dstfinfo->filename);
			srcfinfo->failcnt++;
		}
		free(dstfinfo->filename);
		free(dstfinfo);
		free_inetconfent(iconf);
	}
	(void) fclose(srcfinfo->fp);
	convert_err = srcfinfo->failcnt;

	/* Update hash only if not in enable mode, and only if importing */
	if (!enable && import && (update_hash(srcfinfo->filename) != 0))
		import_err++;

	free(srcfinfo);

	if (enable_err != 0)
		return (EXIT_ERROR_ENBL);
	if (import_err != 0)
		return (EXIT_ERROR_IMP);
	if (convert_err != 0)
		return (EXIT_ERROR_CONV);
	return (EXIT_SUCCESS);
}
